昨天聊完為什麼我要自己做一套繁體中文公文 OCR,今天先不急著講 CRAFT、PaliGemma2 或 LoRA。
因為在開始調模型之前,我遇到一個更基本的問題:
到底怎樣才叫做 OCR 很準?
很多 OCR 產品或研究都會出現類似這樣的描述:
辨識準確率 98%、99%,甚至 99.5%。
乍看之下很好理解。
99% 準確率,不就是 100 個字只錯 1 個嗎?
但真的開始做 OCR Benchmark 後,我才發現:
單獨看到一個「99%」,其實幾乎沒有意義。
你至少還要知道三件事情:
哪個模型、考哪一批文件、用什麼方式計分。
少掉其中一個,兩個百分比很可能根本不能直接比較。
這個專案主要處理繁體中文,因此我最主要看的指標是:
CER(Character Error Rate,字元錯誤率)
它的概念其實很直覺。
把 OCR 輸出的文字跟正確答案相比,看看需要修改幾次才能變成正確答案。
公式是:
CER = (S + D + I) / N
其中:
S(Substitution) 是把字認錯。
例如:
正確:環境保護
辨識:環境保謢
「護」被認成「謢」,算一次替換。
D(Deletion) 是漏掉原本應該存在的字。
正確:
環境保護署
辨識:
環境保署
少了一個「護」,算一次刪除。
I(Insertion) 則是多辨識出本來不存在的字。
正確:
環境保護署
辨識:
環境境保護署
多了一個「境」,算一次插入。
最後的 N,就是正確答案原本有多少個字元。
所以如果一份文件總共有 10,000 個字,總共發生 100 次錯誤:
CER = 100 / 10000 = 1%
大致可以理解成:
每 100 個字錯 1 個。
或者:
每 1,000 個字錯 10 個。
這也是我一開始替自己定下來的目標:
CER < 1%
假設今天有一頁文件,OCR 把每一行的文字都辨識得非常準。
這代表整頁 OCR 就很準嗎?
不一定。
因為昨天有提到,我這套 OCR 並不是:
圖片 → Model → 文字
而是一整條 Pipeline。
前面還有:
文字偵測、版面分析、表格切割、閱讀順序判斷。
所以可能發生一件很有趣的事情:
每一行字都辨識對了,但整頁內容還是錯的。
例如原本閱讀順序是:
第一段 → 第二段 → 第三段
結果版面分析組成:
第二段 → 第一段 → 第三段
如果我只拿「成功切出來的文字行」去算 CER,模型看起來可能超準。
但使用者最後拿到的文件卻是亂的。
所以後來我發現:
OCR 的 Benchmark 不只要決定怎麼算錯字,還要先決定「在哪一層算」。
最單純的是 Crop CER。
把一小塊已經裁好的文字圖片直接丟給模型。
例如:
[ 環境部環境管理署 ]
然後只看模型有沒有讀對。
這種方式非常適合評估:
「辨識模型本身會不會認字?」
但是它完全不管前面的 Detection 有沒有漏字,也不管閱讀順序。
第二種是 Matched CER。
先讓 OCR 自己找文字,再把成功找到、而且可以跟 Ground Truth 配對的文字行拿出來比較。
這會比 Crop 更接近真實流程。
但它還是存在一個很大的問題:
沒找到的東西可能根本沒進入計分。
例如原本有 100 行文字,OCR 只找到 90 行。
如果只拿那成功配對的 90 行去算,可能會得到一個非常漂亮的 CER。
但另外消失的 10 行呢?
對使用者來說,那才是最大的問題。
我現在最主要看的指標叫:
Page CER
做法很直接。
把 Ground Truth 整頁的文字按照正確閱讀順序串起來。
OCR 的輸出也按照系統判斷的閱讀順序串起來。
然後兩邊直接算 CER。
這樣一來:
辨識錯字會被算進去。
漏掉文字會被算進去。
多出文字會被算進去。
連閱讀順序錯誤也會反映在結果裡。
也就是說,Page CER 測的已經不只是 PaliGemma2。
它測的是:
「這整套 OCR Pipeline 最後到底交出了什麼東西給使用者?」
這才是我真正關心的數字。
做到目前,我這套系統有一個看起來非常漂亮的數字:
Page CER = 0.3850%
換算一下,大概就是:
每 1,000 個字錯 4 個左右。
而最初版本的錯誤率是:
5.4164%
一路調到最後的 0.3850%。
降幅超過 90%。
如果我要做一張漂亮的簡報,到這裡其實就可以停了。
但是問題來了。
這個 0.3850% 到底是在哪一份資料上得到的?
答案是:
我的固定回歸測試集。
這一組包含 20 份文件、40 頁,共 25,715 個字元。
它最大的用途是:
每次我改模型、改前處理、改後處理,都拿同一批文件重新跑,確認系統沒有被改壞。
這對工程開發非常重要。
但是它有一個致命問題:
我已經看過它很多次了。
這是一個我做這個專案之後越來越在意的觀念。
假設我今天改了一個演算法。
跑固定測試集:
CER 下降。
我就保留這個修改。
明天又改一版。
CER 上升。
於是我把修改撤掉。
重複幾十次之後會發生什麼?
雖然我從來沒有直接拿測試集去 Training,
但是:
我的工程決策一直在根據這份測試集做調整。
久而久之,整套系統其實已經間接針對這張考卷最佳化。
所以我後來把它叫做:
固定回歸測試集。
而不是「完全沒看過的測試集」。
它可以回答:
「這一版有沒有比上一版退步?」
但不能完全回答:
「明天隨便丟一份沒看過的政府公文進來,也會有 0.385% CER 嗎?」
這是兩個完全不同的問題。
要回答第二個問題,就需要另外準備:
Holdout Set,保留集。
概念很簡單。
這批資料:
模型訓練時不能看。
調參時不能看。
做產品修正時也不要一直拿出來看。
一直到真的想知道:
「現在這個版本遇到陌生文件到底表現怎樣?」
才拿出來考一次。
我實際測到的結果就很有意思。
固定回歸集:
0.3850%
但另外兩批陌生文件的結果分別是:
holdout_v3:1.0396%
holdout_v4:1.2585%
這時候你會發現:
0.385% 沒有錯。
1.04% 也沒有錯。
1.26% 也沒有錯。
它們回答的是不同問題。
比較精確的說法應該是:
目前 v10 在固定回歸測試集上的整頁 CER 為 0.3850%。
而如果問:
「那陌生公文呢?」
目前的證據只能說:
在兩批未參與訓練與先前調整的文件上,Page CER 約為 1.04% 與 1.26%。
所以至少以現在的實驗,我還不能直接宣稱:
「所有陌生政府公文都能做到 CER < 1%。」
這句話看起來沒有「99.6% 準確率!」那麼帥。
但我認為這才是 Benchmark 真正應該呈現的方式。
後來我還遇到更麻煩的事情。
某一階段,我的開發集成績改善了大約 39%。
看起來模型進步非常大。
但真正拿保留集測試時:
改善幅度只有 8.6%。
為什麼?
回頭檢查才發現:
當時的開發集主要都是一般橫排文章。
但真正的政府公文裡還有:
密集表格、直書、多欄版面、複雜閱讀順序。
也就是說:
考卷出的題目跟產品真正會遇到的題目不一樣。
這時候模型在 Benchmark 上進步很多,使用者卻不一定會有同樣的感受。
這件事情後來對我的影響很大。
因為它代表:
Benchmark 不只是拿資料來測試,Benchmark 本身也是一個需要被設計的產品。
最後還有一個我現在不太敢忽略的問題。
假設兩個版本 CER 都是 1%。
它們真的一樣好嗎?
版本 A:
偶爾把「護」認成「謢」。
版本 B:
偶爾整行文字直接消失。
數學上最後可能都是差不多的錯誤字數。
但是如果今天 OCR 後面要接 RAG 或 AI 審查,
整行消失的破壞性遠比單字 typo 嚴重。
所以我現在每次修改系統,不只是看 CER。
還會一起看錯誤分布:
替換、刪除、插入、漏行。
目的就是避免:
總分沒變,但是我們只是把一種錯誤換成了另一種更嚴重的錯誤。
今天如果只記得一句話,我希望是:
不要問「這個 OCR 準確率多少」,先問「在哪份資料、用什麼單位、怎麼算的」。
對我來說,目前最重要的尺度是:
Page CER
因為我不是只想知道模型會不會認字。
我要知道的是:
從找文字、判斷版面、辨識,到最後組回整份文件,使用者真正拿到的東西到底有多準。
而 0.3850% 這個數字也是真的。
只是它代表的是:
固定回歸集上的成果。
不是所有未來文件的保證。
下一篇 Day 3,我就可以正式把整套系統攤開來看:
為什麼我最後沒有選擇讓一個 VLM 從頭做到尾,而是拆成文字偵測、版面分析、表格解析與文字辨識四層。
也會開始介紹這套系統裡第一個真正工作的模型:
CRAFT。